# coding=utf8
import multiprocessing
import os
import struct
import sys
import time

from pyjvm.exceptions import CloneNotSupportedException, NullPointException, ArrayStoreException, \
    IndexOutOfBoundsException
from pyjvm.instructions.common import ClassInitializer
from pyjvm.instructions.invoke import invoke_method
from pyjvm.instructions.invoke import register
from pyjvm.runtime.heap import JObject, Heap, py_classes_to_j_array_obj, py_bytes_to_j_bytes_obj
from pyjvm.runtime.loader import ClassLoader
from pyjvm.runtime.metadata import Class as ClassMetadata
from pyjvm.runtime.strpool import StringPool
from pyjvm.runtime.thread import Frame, OperandStack, Thread as PThread
from ctypes import c_ubyte


# java.io 包下本地方法


class FileDescriptor:

    @staticmethod
    def set(frame: Frame):
        frame.operand_stack.push(0)


register('java/io/FileDescriptor', "set", "(I)J", FileDescriptor.set)


class FileOutputStream:

    @staticmethod
    def writeBytes(frame: Frame):
        _vars = frame.local_vars
        j_bytes = _vars[1].data
        offset = _vars[2]
        length = _vars[3]
        py_bytes = bytes(b if b > -1 else c_ubyte(b).value for b in j_bytes[offset: offset + length])
        sys.stdout.write(py_bytes.decode('u8'))


register('java/io/FileOutputStream', "writeBytes", "([BIIZ)V", FileOutputStream.writeBytes)


class UnixFileSystem:
    NAME = 'java/io/UnixFileSystem'

    @staticmethod
    def canonicalize0(frame: Frame):
        j_path = frame.local_vars[1]

        py_path = StringPool.to_py_str(j_path)
        py_path2 = os.path.normpath(py_path)

        if py_path != py_path2:
            j_path = StringPool.new(frame.method.klass.loader, py_path2)

        frame.operand_stack.push(j_path)

    @staticmethod
    def _get_path(file_obj: JObject):
        path_str = file_obj.get_field_value('path', 'Ljava/lang/String;')
        return StringPool.to_py_str(path_str)

    @staticmethod
    def getBooleanAttributes0(frame: Frame):
        file_obj = frame.local_vars[1]
        path = UnixFileSystem._get_path(file_obj)

        attributes0 = 0
        if os.path.exists(path):
            attributes0 |= 0x01

        if os.path.isdir(path):
            attributes0 |= 0x04

        frame.operand_stack.push(attributes0)


register(UnixFileSystem.NAME, "canonicalize0", "(Ljava/lang/String;)Ljava/lang/String;", UnixFileSystem.canonicalize0)
register(UnixFileSystem.NAME, "getBooleanAttributes0", "(Ljava/io/File;)I", UnixFileSystem.getBooleanAttributes0)
register('java/io/WinNTFileSystem', "getBooleanAttributes", "(Ljava/io/File;)I", UnixFileSystem.getBooleanAttributes0)
register('java/io/WinNTFileSystem', "canonicalize0", "(Ljava/lang/String;)Ljava/lang/String;",
         UnixFileSystem.canonicalize0)


# java.lang 包下


class Class:
    NAME = 'java/lang/Class'
    _CONSTRUCTOR_CONSTRUCTOR_DESCRIPTOR = "" + \
                                          "(Ljava/lang/Class;" + \
                                          "[Ljava/lang/Class;" + \
                                          "[Ljava/lang/Class;" + \
                                          "II" + \
                                          "Ljava/lang/String;" + \
                                          "[B[B)V"

    _FIELD_CONSTRUCTOR_DESCRIPTOR = "" + \
                                    "(Ljava/lang/Class;" + \
                                    "Ljava/lang/String;" + \
                                    "Ljava/lang/Class;" + \
                                    "II" + \
                                    "Ljava/lang/String;" + \
                                    "[B)V"

    _METHOD_CONSTRUCTOR_DESCRIPTOR = "" + \
                                     "(Ljava/lang/Class;" + \
                                     "Ljava/lang/String;" + \
                                     "[Ljava/lang/Class;" + \
                                     "Ljava/lang/Class;" + \
                                     "[Ljava/lang/Class;" + \
                                     "II" + \
                                     "Ljava/lang/String;" + \
                                     "[B[B[B)V"

    @staticmethod
    def getPrimitiveClass(frame: Frame):
        name_obj = frame.local_vars[0]
        name = StringPool.to_py_str(name_obj)

        loader: ClassLoader = frame.method.klass.loader
        klass = loader.load_class(name).j_class

        frame.operand_stack.push(klass)

    @staticmethod
    def getName0(frame: Frame):
        this = frame.local_vars[0]
        klass = this.extra

        name = klass.get_java_name()
        name_obj = StringPool.new(klass.loader, name)

        frame.operand_stack.push(name_obj)

    @staticmethod
    def desiredAssertionStatus0(frame: Frame):
        frame.operand_stack.push(False)

    @staticmethod
    def isInterface(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra

        frame.operand_stack.push(klass.is_interface)

    @staticmethod
    def isPrimitive(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra

        frame.operand_stack.push(klass.is_primitive)

    @staticmethod
    def forName0(frame: Frame):
        j_name = frame.local_vars[0]
        initialize = frame.local_vars[1]

        py_name = StringPool.to_py_str(j_name).replace('.', '/')
        klass: ClassMetadata = frame.method.klass.loader.load_class(py_name)
        j_class = klass.j_class

        if initialize and not klass.inited:
            thread = frame.thread
            frame.set_next_pc(thread.pc)
            ClassInitializer.init_class(thread, klass)
        else:
            frame.operand_stack.push(j_class)

    @staticmethod
    def getModifiers(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra

        frame.operand_stack.push(klass.access_flags)

    @staticmethod
    def getSuperclass(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra
        super_class = klass.super_class

        frame.operand_stack.push(None if super_class is None
                                 else super_class.j_class)

    @staticmethod
    def getInterfaces0(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra
        interfaces = klass.interfaces

        item_class: ClassMetadata = klass.loader.load_class('java/lang/Class')
        j_interfaces = Heap.new_from_data(item_class.get_component_array_class(),
                                          [i.j_class for i in interfaces])

        frame.operand_stack.push(j_interfaces)

    @staticmethod
    def isArray(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra

        frame.operand_stack.push(klass.is_array_class)

    @staticmethod
    def getComponentType(frame: Frame):
        this = frame.local_vars[0]
        klass: ClassMetadata = this.extra

        frame.operand_stack.push(klass.get_component_class().j_class)

    @staticmethod
    def isAssignableFrom(frame: Frame):
        this = frame.local_vars[0]
        cls = frame.local_vars[1]

        ret = this.extra.accessible_from(cls.extra)
        frame.operand_stack.push(ret)

    @staticmethod
    def getDeclaredConstructors0(frame: Frame):
        this = frame.local_vars[0]
        public_only = frame.local_vars[1]
        klass: ClassMetadata = this.extra

        constructors = klass.get_constructors(public_only)
        loader = frame.method.klass.loader
        constructor_class: ClassMetadata = loader.load_class('java/lang/reflect/Constructor')
        j_constructors_ref = Heap.new_array(constructor_class.get_component_array_class(), len(constructors))

        frame.operand_stack.push(j_constructors_ref)

        if constructors:
            thread = frame.thread
            j_constructors = j_constructors_ref.data
            constructor_method = constructor_class.get_constructor(
                Class._CONSTRUCTOR_CONSTRUCTOR_DESCRIPTOR)
            for i, constructor in enumerate(constructors):
                j_constructor = Heap.new_object(constructor_class)
                j_constructor.extra = constructor
                j_constructors[i] = j_constructor

                ops = OperandStack(9)
                ops.push(j_constructor)
                ops.push(this)
                ops.push(py_classes_to_j_array_obj(loader, constructor.get_parameter_types()))
                ops.push(py_classes_to_j_array_obj(loader, constructor.get_exception_types()))
                ops.push(constructor.access_flags)
                ops.push(0)
                if constructor.signature is None:
                    ops.push(None)
                else:
                    ops.push(StringPool.new(loader, constructor.signature))
                ops.push(py_bytes_to_j_bytes_obj(loader, constructor.annotation_bytes))
                ops.push(py_bytes_to_j_bytes_obj(loader, constructor.parameter_annotation_bytes))

                shim = Frame.new_shim(thread, ops)
                thread.push_frame(shim)
                invoke_method(shim, constructor_method)

    @staticmethod
    def getDeclaredFields0(frame: Frame):
        this = frame.local_vars[0]
        public_only = frame.local_vars[1]
        klass: ClassMetadata = this.extra

        fields = klass.get_fields(public_only)
        field_count = len(fields)

        loader = frame.method.klass.loader
        field_class: ClassMetadata = loader.load_class('java/lang/reflect/Field')
        j_field__arr = Heap.new_array(field_class.get_component_array_class(), field_count)

        frame.operand_stack.push(j_field__arr)

        if field_count:
            thread = frame.thread
            j_fields = j_field__arr.data

            field_constructor = field_class.get_constructor(Class._FIELD_CONSTRUCTOR_DESCRIPTOR)
            for i, field in enumerate(fields):
                j_field = Heap.new_object(field_class)
                j_field.extra = field
                j_fields[i] = j_field

                ops = OperandStack(8)
                ops.push(j_field)
                ops.push(this)
                ops.push(StringPool.new(loader, field.name))
                ops.push(field.get_type().j_class)
                ops.push(field.access_flags)
                ops.push(id(field))
                if field.signature is None:
                    ops.push(None)
                else:
                    ops.push(StringPool.new(loader, field.signature))
                ops.push(py_bytes_to_j_bytes_obj(loader, field.annotation_bytes))

                shim = Frame.new_shim(thread, ops)
                thread.push_frame(shim)
                invoke_method(shim, field_constructor)

    @staticmethod
    def getDeclaredMethods0(frame: Frame):
        this = frame.local_vars[0]
        public_only = frame.local_vars[1]
        klass: ClassMetadata = this.extra

        methods = klass.get_methods(public_only)
        method_count = len(methods)

        loader = klass.loader
        method_class: ClassMetadata = loader.load_class('java/lang/reflect/Method')
        j_method_arr = Heap.new_array(method_class.get_component_array_class(), method_count)
        frame.operand_stack.push(j_method_arr)

        if method_count:
            thread = frame.thread
            j_methods = j_method_arr.data
            method_constructor = method_class.get_constructor(Class._METHOD_CONSTRUCTOR_DESCRIPTOR)

            for i, method in enumerate(methods):
                j_method = Heap.new_object(method_class)
                j_method.extra = method
                j_methods[i] = j_method

                ops = OperandStack(12)
                ops.push(j_method)
                ops.push(this)
                ops.push(StringPool.new(loader, method.name))
                ops.push(py_classes_to_j_array_obj(loader, method.get_parameter_types()))
                ops.push(method.get_return_type().j_class)
                ops.push(py_classes_to_j_array_obj(loader, method.get_exception_types()))
                ops.push(method.access_flags)
                ops.push(0)
                if method.signature is None:
                    ops.push(None)
                else:
                    ops.push(StringPool.new(loader, method.signature))
                ops.push(py_bytes_to_j_bytes_obj(loader, method.annotation_bytes))
                ops.push(py_bytes_to_j_bytes_obj(loader, method.parameter_annotation_bytes))
                ops.push(py_bytes_to_j_bytes_obj(loader, method.annotation_default_bytes))

                shim = Frame.new_shim(thread, ops)
                thread.push_frame(shim)
                invoke_method(shim, method_constructor)


register(Class.NAME, "getPrimitiveClass", "(Ljava/lang/String;)Ljava/lang/Class;", Class.getPrimitiveClass)
register(Class.NAME, "getName0", "()Ljava/lang/String;", Class.getName0)
register(Class.NAME, "desiredAssertionStatus0", "(Ljava/lang/Class;)Z", Class.desiredAssertionStatus0)
register(Class.NAME, "isInterface", "()Z", Class.isInterface)
register(Class.NAME, "isPrimitive", "()Z", Class.isPrimitive)
register(Class.NAME, "getDeclaredFields0", "(Z)[Ljava/lang/reflect/Field;", Class.getDeclaredFields0)
register(Class.NAME, "forName0", "(Ljava/lang/String;ZLjava/lang/ClassLoader;Ljava/lang/Class;)Ljava/lang/Class;",
         Class.forName0)
register(Class.NAME, "getDeclaredConstructors0", "(Z)[Ljava/lang/reflect/Constructor;", Class.getDeclaredConstructors0)
register(Class.NAME, "getModifiers", "()I", Class.getModifiers)
register(Class.NAME, "getSuperclass", "()Ljava/lang/Class;", Class.getSuperclass)
register(Class.NAME, "getInterfaces0", "()[Ljava/lang/Class;", Class.getInterfaces0)
register(Class.NAME, "isArray", "()Z", Class.isArray)
register(Class.NAME, "getDeclaredMethods0", "(Z)[Ljava/lang/reflect/Method;", Class.getDeclaredMethods0)
register(Class.NAME, "getComponentType", "()Ljava/lang/Class;", Class.getComponentType)
register(Class.NAME, "isAssignableFrom", "(Ljava/lang/Class;)Z", Class.isAssignableFrom)


class Object:
    NAME = 'java/lang/Object'

    @staticmethod
    def getClass(frame: Frame):
        this = frame.local_vars[0]
        j_class = this.klass.j_class
        frame.operand_stack.push(j_class)

    @staticmethod
    def hashCode(frame: Frame):
        this = frame.local_vars[0]
        frame.operand_stack.push(id(this))

    @staticmethod
    def clone(frame: Frame):
        this = frame.local_vars[0]
        cloneable = this.klass.loader.load_class('java/lang/Cloneable')
        if not this.klass.is_implements(cloneable):
            raise CloneNotSupportedException()
        frame.operand_stack.push(this.clone())

    @staticmethod
    def notifyAll(frame: Frame):
        pass


register(Object.NAME, "getClass", "()Ljava/lang/Class;", Object.getClass)
register(Object.NAME, "hashCode", "()I", Object.hashCode)
register(Object.NAME, "clone", "()Ljava/lang/Object;", Object.clone)
register(Object.NAME, "notifyAll", "()V", Object.notifyAll)


class Double:
    NAME = 'java/lang/Double'

    @staticmethod
    def doubleToRawLongBits(frame: Frame):
        double = frame.local_vars[0]
        long = struct.unpack('q', struct.pack('d', double))[0]
        frame.operand_stack.push(long)

    @staticmethod
    def longBitsToDouble(frame: Frame):
        long = frame.local_vars[0]
        double = struct.unpack('d', struct.pack('q', long))[0]
        frame.operand_stack.push(double)


register(Double.NAME, "doubleToRawLongBits", "(D)J", Double.doubleToRawLongBits)
register(Double.NAME, "longBitsToDouble", "(J)D", Double.longBitsToDouble)


class Float:
    NAME = 'java/lang/Float'

    @staticmethod
    def floatToRawIntBits(frame: Frame):
        float_val = frame.local_vars[0]
        int_val = struct.unpack('i', struct.pack('f', float_val))[0]
        frame.operand_stack.push(int_val)

    @staticmethod
    def intBitsToFloat(frame: Frame):
        int_val = frame.local_vars[0]
        float_val = struct.unpack('f', struct.pack('i', int_val))[0]
        frame.operand_stack.push(float_val)


register(Float.NAME, "floatToRawIntBits", "(F)I", Float.floatToRawIntBits)
register(Float.NAME, "intBitsToFloat", "(I)F", Float.intBitsToFloat)


class String:
    NAME = 'java/lang/String'

    @staticmethod
    def intern(frame: Frame):
        this = frame.local_vars[0]
        interned = StringPool.intern(this)
        frame.operand_stack.push(interned)


register(String.NAME, "intern", "()Ljava/lang/String;", String.intern)


class Runtime:
    NAME = 'java/lang/Runtime'

    @staticmethod
    def availableProcessors(frame: Frame):
        num_cpu = multiprocessing.cpu_count()
        frame.operand_stack.push(num_cpu)


register(Runtime.NAME, "availableProcessors", "()I", Runtime.availableProcessors)


class System:
    NAME = 'java/lang/System'

    @staticmethod
    def checkArrayCopy(src, dest) -> bool:

        src_class: ClassMetadata = src.klass
        dest_class: ClassMetadata = dest.klass

        if not src_class.is_array_class or not dest_class.is_array_class:
            return False

        if src_class.get_component_class().is_primitive or dest_class.get_component_class().is_primitive:
            return src_class is dest_class

        return True

    @staticmethod
    def nanoTime(frame: Frame):
        frame.operand_stack.push(time.time_ns())

    @staticmethod
    def arraycopy(frame: Frame):
        _vars = frame.local_vars
        src = _vars[0]
        src_pos = _vars[1]
        dest = _vars[2]
        dest_pos = _vars[3]
        length = _vars[4]

        if src is None or dest is None:
            raise NullPointException()

        if not System.checkArrayCopy(src, dest):
            raise ArrayStoreException()

        if src_pos < 0 or dest_pos < 0 or length < 0 or \
                src_pos + length > len(src.data) or \
                dest_pos + length > len(dest.data):
            raise IndexOutOfBoundsException()

        dest.data[dest_pos: dest_pos + length] = src.data[src_pos: src_pos + length]

    SYS_PROPS = {
        "java.version": "1.8.0",
        "java.vendor": "jvm.go",
        "java.vendor.url": "https://github.com/zxh0/jvm.go",
        "java.home": "todo",
        "java.class.version": "52.0",
        "java.class.path": "todo",
        "java.awt.graphicsenv": "sun.awt.CGraphicsEnvironment",
        "os.name": os.name,  #
        "os.arch": '',  # todo
        "os.version": "",  # todo
        "file.separator": os.path.sep,  #
        "path.separator": os.path.pathsep,  #
        "line.separator": "\n",  # todo
        "user.name": "",  # todo
        "user.home": "",  # todo
        "user.dir": ".",  # todo
        "user.country": "CN",  # todo
        "file.encoding": "UTF-8",
        "sun.stdout.encoding": "UTF-8",
        "sun.stderr.encoding": "UTF-8",
    }

    @staticmethod
    def initProperties(frame: Frame):
        props = frame.local_vars[0]
        frame.operand_stack.push(props)

        set_prop = props.klass.get_instance_method(
            "setProperty",
            "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;")

        thread = frame.thread
        loader = frame.method.klass.loader
        for key, val in System.SYS_PROPS.items():
            j_key = StringPool.new(loader, key)
            j_val = StringPool.new(loader, val)
            ops = OperandStack(3)
            ops.push(props)
            ops.push(j_key)
            ops.push(j_val)

            shim = Frame.new_shim(thread, ops)
            thread.push_frame(shim)
            invoke_method(shim, set_prop)

    @staticmethod
    def setIn0(frame: Frame):
        in_ref = frame.local_vars[0]
        klass: ClassMetadata = frame.method.klass
        klass.set_static_field('in', 'Ljava/io/InputStream;', in_ref)

    @staticmethod
    def setOut0(frame: Frame):
        out = frame.local_vars[0]
        klass: ClassMetadata = frame.method.klass
        klass.set_static_field('out', 'Ljava/io/PrintStream;', out)

    @staticmethod
    def setErr0(frame: Frame):
        err = frame.local_vars[0]
        klass: ClassMetadata = frame.method.klass
        klass.set_static_field('err', 'Ljava/io/PrintStream;', err)

    @staticmethod
    def currentTimeMillis(frame: Frame):
        millis = int(time.time() * 1000)
        frame.operand_stack.push(millis)

    @staticmethod
    def mapLibraryName(frame: Frame):
        pass


register(System.NAME, "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", System.arraycopy)
register(System.NAME, "initProperties", "(Ljava/util/Properties;)Ljava/util/Properties;", System.initProperties)
register(System.NAME, "setIn0", "(Ljava/io/InputStream;)V", System.setIn0)
register(System.NAME, "setOut0", "(Ljava/io/PrintStream;)V", System.setOut0)
register(System.NAME, "setErr0", "(Ljava/io/PrintStream;)V", System.setErr0)
register(System.NAME, "currentTimeMillis", "()J", System.currentTimeMillis)
register(System.NAME, "nanoTime", "()J", System.nanoTime)


class Thread:
    NAME = 'java/lang/Thread'

    @staticmethod
    def currentThread(frame: Frame):
        loader = frame.method.klass.loader
        thread_class = loader.load_class('java/lang/Thread')
        j_thread = Heap.new_object(thread_class)

        thread_group_class = loader.load_class('java/lang/ThreadGroup')
        j_group = Heap.new_object(thread_group_class)

        j_thread.put_field_value('group', 'Ljava/lang/ThreadGroup;', j_group)
        j_thread.put_field_value('priority', 'I', 1)

        frame.operand_stack.push(j_thread)

    @staticmethod
    def setPriority0(frame: Frame):
        pass

    @staticmethod
    def isAlive(frame: Frame):
        frame.operand_stack.push(False)

    @staticmethod
    def start0(frame: Frame):
        pass


register(Thread.NAME, "currentThread", "()Ljava/lang/Thread;", Thread.currentThread)
register(Thread.NAME, "setPriority0", "(I)V", Thread.setPriority0)
register(Thread.NAME, "isAlive", "()Z", Thread.isAlive)
register(Thread.NAME, "start0", "()V", Thread.start0)


class StackTraceElement:

    def __init__(self):
        self.file_name = None
        self.class_name = None
        self.method_name = None
        self.line_number = None
        self.method_descriptor = None

    def __str__(self):
        return '%s.%s%s(%s:%d)' % (
            self.class_name, self.method_name, self.method_descriptor, self.file_name, self.line_number)


class Throwable:
    NAME = 'java/lang/Throwable'

    @staticmethod
    def distanceToObject(klass: ClassMetadata):
        distance = 0
        c = klass.super_class
        while c is not None:
            distance += 1
            c = c.super_class
        return distance

    @staticmethod
    def createStackTraceElements(tobj: JObject, thread: PThread):
        skip = Throwable.distanceToObject(tobj.klass) + 2
        frames = thread.get_frames()[skip:]
        return [Throwable.createStackTraceElement(frame) for frame in frames]

    @staticmethod
    def createStackTraceElement(frame: Frame):
        method = frame.method
        klass = method.klass
        trace_element = StackTraceElement()
        trace_element.file_name = klass.source_file
        trace_element.class_name = klass.get_java_name()
        trace_element.method_name = method.name
        trace_element.line_number = method.get_line_number(frame.next_pc - 1)
        trace_element.method_descriptor = method.descriptor
        return trace_element

    @staticmethod
    def fillInStackTrace(frame: Frame):
        this = frame.local_vars[0]
        frame.operand_stack.push(this)
        this.extra = Throwable.createStackTraceElements(this, frame.thread)


register(Throwable.NAME, "fillInStackTrace", "(I)Ljava/lang/Throwable;", Throwable.fillInStackTrace)


class AccessController:
    NAME = 'java/security/AccessController'

    @staticmethod
    def doPrivileged(frame: Frame):
        action = frame.local_vars[0]
        frame.operand_stack.push(action)
        method = action.klass.get_instance_method('run', '()Ljava/lang/Object;')
        invoke_method(frame, method)

    @staticmethod
    def getStackAccessControlContext(frame: Frame):
        frame.operand_stack.push(None)
        # todo


register(AccessController.NAME, "doPrivileged", "(Ljava/security/PrivilegedAction;)Ljava/lang/Object;",
         AccessController.doPrivileged)
register(AccessController.NAME, "doPrivileged", "(Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;",
         AccessController.doPrivileged)
register(AccessController.NAME, "getStackAccessControlContext", "()Ljava/security/AccessControlContext;",
         AccessController.getStackAccessControlContext)


class AtomicLong:
    NAME = 'java/util/concurrent/atomic/AtomicLong'

    @staticmethod
    def vmSupportsCS8(frame: Frame):
        frame.operand_stack.push(False)


register(AtomicLong.NAME, "VMSupportsCS8", "()Z", AtomicLong.vmSupportsCS8)
